package de.danoeh.antennapod.core.service.playback; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.bluetooth.BluetoothA2dp; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.graphics.Bitmap; import android.graphics.BitmapFactory; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.NetworkInfo; import android.net.wifi.WifiManager; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.Vibrator; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.support.annotation.StringRes; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import android.support.v7.app.NotificationCompat; import android.support.v7.media.MediaRouter; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import android.view.Display; import android.view.InputDevice; import android.view.KeyEvent; import android.view.SurfaceHolder; import android.view.WindowManager; import android.widget.Toast; import com.bumptech.glide.Glide; import com.bumptech.glide.request.target.Target; import com.google.android.gms.cast.ApplicationMetadata; import com.google.android.libraries.cast.companionlibrary.cast.BaseCastManager; import java.util.List; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.cast.CastConsumer; import de.danoeh.antennapod.core.cast.CastManager; import de.danoeh.antennapod.core.cast.DefaultCastConsumer; import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.MediaType; import de.danoeh.antennapod.core.glide.ApGlideSettings; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction; import de.danoeh.antennapod.core.gpoddernet.model.GpodnetEpisodeAction.Action; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.receiver.MediaButtonReceiver; import de.danoeh.antennapod.core.storage.DBTasks; import de.danoeh.antennapod.core.storage.DBWriter; import de.danoeh.antennapod.core.util.IntList; import de.danoeh.antennapod.core.util.NetworkUtils; import de.danoeh.antennapod.core.util.QueueAccess; import de.danoeh.antennapod.core.util.flattr.FlattrUtils; import de.danoeh.antennapod.core.util.playback.ExternalMedia; import de.danoeh.antennapod.core.util.playback.Playable; /** * Controls the MediaPlayer that plays a FeedMedia-file */ public class PlaybackService extends Service { public static final String FORCE_WIDGET_UPDATE = "de.danoeh.antennapod.FORCE_WIDGET_UPDATE"; public static final String STOP_WIDGET_UPDATE = "de.danoeh.antennapod.STOP_WIDGET_UPDATE"; /** * Logging tag */ private static final String TAG = "PlaybackService"; /** * Parcelable of type Playable. */ public static final String EXTRA_PLAYABLE = "PlaybackService.PlayableExtra"; /** * True if cast session should disconnect. */ public static final String EXTRA_CAST_DISCONNECT = "extra.de.danoeh.antennapod.core.service.castDisconnect"; /** * True if media should be streamed. */ public static final String EXTRA_SHOULD_STREAM = "extra.de.danoeh.antennapod.core.service.shouldStream"; /** * True if playback should be started immediately after media has been * prepared. */ public static final String EXTRA_START_WHEN_PREPARED = "extra.de.danoeh.antennapod.core.service.startWhenPrepared"; public static final String EXTRA_PREPARE_IMMEDIATELY = "extra.de.danoeh.antennapod.core.service.prepareImmediately"; public static final String ACTION_PLAYER_STATUS_CHANGED = "action.de.danoeh.antennapod.core.service.playerStatusChanged"; public static final String EXTRA_NEW_PLAYER_STATUS = "extra.de.danoeh.antennapod.service.playerStatusChanged.newStatus"; private static final String AVRCP_ACTION_PLAYER_STATUS_CHANGED = "com.android.music.playstatechanged"; private static final String AVRCP_ACTION_META_CHANGED = "com.android.music.metachanged"; public static final String ACTION_PLAYER_NOTIFICATION = "action.de.danoeh.antennapod.core.service.playerNotification"; public static final String EXTRA_NOTIFICATION_CODE = "extra.de.danoeh.antennapod.core.service.notificationCode"; public static final String EXTRA_NOTIFICATION_TYPE = "extra.de.danoeh.antennapod.core.service.notificationType"; /** * If the PlaybackService receives this action, it will stop playback and * try to shutdown. */ public static final String ACTION_SHUTDOWN_PLAYBACK_SERVICE = "action.de.danoeh.antennapod.core.service.actionShutdownPlaybackService"; /** * If the PlaybackService receives this action, it will end playback of the * current episode and load the next episode if there is one available. */ public static final String ACTION_SKIP_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.skipCurrentEpisode"; /** * If the PlaybackService receives this action, it will pause playback. */ public static final String ACTION_PAUSE_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.pausePlayCurrentEpisode"; /** * If the PlaybackService receives this action, it will resume playback. */ public static final String ACTION_RESUME_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.resumePlayCurrentEpisode"; /** * Used in NOTIFICATION_TYPE_RELOAD. */ public static final int EXTRA_CODE_AUDIO = 1; public static final int EXTRA_CODE_VIDEO = 2; public static final int EXTRA_CODE_CAST = 3; public static final int NOTIFICATION_TYPE_ERROR = 0; public static final int NOTIFICATION_TYPE_INFO = 1; public static final int NOTIFICATION_TYPE_BUFFER_UPDATE = 2; /** * Receivers of this intent should update their information about the curently playing media */ public static final int NOTIFICATION_TYPE_RELOAD = 3; /** * The state of the sleeptimer changed. */ public static final int NOTIFICATION_TYPE_SLEEPTIMER_UPDATE = 4; public static final int NOTIFICATION_TYPE_BUFFER_START = 5; public static final int NOTIFICATION_TYPE_BUFFER_END = 6; /** * No more episodes are going to be played. */ public static final int NOTIFICATION_TYPE_PLAYBACK_END = 7; /** * Playback speed has changed */ public static final int NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE = 8; /** * Ability to set the playback speed has changed */ public static final int NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED = 9; /** * Send a message to the user (with provided String resource id) */ public static final int NOTIFICATION_TYPE_SHOW_TOAST = 10; /** * Returned by getPositionSafe() or getDurationSafe() if the playbackService * is in an invalid state. */ public static final int INVALID_TIME = -1; /** * Time in seconds during which the CastManager will try to reconnect to the Cast Device after * the Wifi Connection is regained. */ private static final int RECONNECTION_ATTEMPT_PERIOD_S = 15; /** * Is true if service is running. */ public static boolean isRunning = false; /** * Is true if service has received a valid start command. */ public static boolean started = false; /** * Is true if the service was running, but paused due to headphone disconnect */ public static boolean transientPause = false; /** * Is true if a Cast Device is connected to the service. */ private static volatile boolean isCasting = false; /** * Stores the state of the cast playback just before it disconnects. */ private volatile PlaybackServiceMediaPlayer.PSMPInfo infoBeforeCastDisconnection; private boolean wifiConnectivity = true; private BroadcastReceiver wifiBroadcastReceiver; private static final int NOTIFICATION_ID = 1; private PlaybackServiceMediaPlayer mediaPlayer; private PlaybackServiceTaskManager taskManager; private CastManager castManager; private MediaRouter mediaRouter; /** * Only used for Lollipop notifications. */ private MediaSessionCompat mediaSession; private int startPosition; private static volatile MediaType currentMediaType = MediaType.UNKNOWN; private final IBinder mBinder = new LocalBinder(); public class LocalBinder extends Binder { public PlaybackService getService() { return PlaybackService.this; } } @Override public boolean onUnbind(Intent intent) { Log.d(TAG, "Received onUnbind event"); return super.onUnbind(intent); } /** * Returns an intent which starts an audio- or videoplayer, depending on the * type of media that is being played. If the playbackservice is not * running, the type of the last played media will be looked up. */ public static Intent getPlayerActivityIntent(Context context) { if (isRunning) { return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, currentMediaType, isCasting); } else { if (PlaybackPreferences.getCurrentEpisodeIsVideo()) { return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.VIDEO, isCasting); } else { return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, MediaType.AUDIO, isCasting); } } } /** * Same as getPlayerActivityIntent(context), but here the type of activity * depends on the FeedMedia that is provided as an argument. */ public static Intent getPlayerActivityIntent(Context context, Playable media) { MediaType mt = media.getMediaType(); return ClientConfig.playbackServiceCallbacks.getPlayerActivityIntent(context, mt, isCasting); } @Override public void onCreate() { super.onCreate(); Log.d(TAG, "Service created."); isRunning = true; registerReceiver(headsetDisconnected, new IntentFilter( Intent.ACTION_HEADSET_PLUG)); registerReceiver(shutdownReceiver, new IntentFilter( ACTION_SHUTDOWN_PLAYBACK_SERVICE)); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { registerReceiver(bluetoothStateUpdated, new IntentFilter( BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)); } registerReceiver(audioBecomingNoisy, new IntentFilter( AudioManager.ACTION_AUDIO_BECOMING_NOISY)); registerReceiver(skipCurrentEpisodeReceiver, new IntentFilter( ACTION_SKIP_CURRENT_EPISODE)); registerReceiver(pausePlayCurrentEpisodeReceiver, new IntentFilter( ACTION_PAUSE_PLAY_CURRENT_EPISODE)); registerReceiver(pauseResumeCurrentEpisodeReceiver, new IntentFilter( ACTION_RESUME_PLAY_CURRENT_EPISODE)); taskManager = new PlaybackServiceTaskManager(this, taskManagerCallback); mediaRouter = MediaRouter.getInstance(getApplicationContext()); PreferenceManager.getDefaultSharedPreferences(this) .registerOnSharedPreferenceChangeListener(prefListener); ComponentName eventReceiver = new ComponentName(getApplicationContext(), MediaButtonReceiver.class); Intent mediaButtonIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); mediaButtonIntent.setComponent(eventReceiver); PendingIntent buttonReceiverIntent = PendingIntent.getBroadcast(this, 0, mediaButtonIntent, PendingIntent.FLAG_UPDATE_CURRENT); mediaSession = new MediaSessionCompat(getApplicationContext(), TAG, eventReceiver, buttonReceiverIntent); try { mediaSession.setCallback(sessionCallback); mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS); } catch (NullPointerException npe) { // on some devices (Huawei) setting active can cause a NullPointerException // even with correct use of the api. // See http://stackoverflow.com/questions/31556679/android-huawei-mediassessioncompat // and https://plus.google.com/+IanLake/posts/YgdTkKFxz7d Log.e(TAG, "NullPointerException while setting up MediaSession"); npe.printStackTrace(); } castManager = CastManager.getInstance(); castManager.addCastConsumer(castConsumer); isCasting = castManager.isConnected(); if (isCasting) { if (UserPreferences.isCastEnabled()) { onCastAppConnected(false); } else { castManager.disconnect(); } } else { mediaPlayer = new LocalPSMP(this, mediaPlayerCallback); } mediaSession.setActive(true); } @Override public void onDestroy() { super.onDestroy(); Log.d(TAG, "Service is about to be destroyed"); isRunning = false; started = false; currentMediaType = MediaType.UNKNOWN; PreferenceManager.getDefaultSharedPreferences(this) .unregisterOnSharedPreferenceChangeListener(prefListener); if (mediaSession != null) { mediaSession.release(); } unregisterReceiver(headsetDisconnected); unregisterReceiver(shutdownReceiver); if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { unregisterReceiver(bluetoothStateUpdated); } unregisterReceiver(audioBecomingNoisy); unregisterReceiver(skipCurrentEpisodeReceiver); unregisterReceiver(pausePlayCurrentEpisodeReceiver); unregisterReceiver(pauseResumeCurrentEpisodeReceiver); castManager.removeCastConsumer(castConsumer); unregisterWifiBroadcastReceiver(); mediaPlayer.shutdown(); taskManager.shutdown(); } @Override public IBinder onBind(Intent intent) { Log.d(TAG, "Received onBind event"); return mBinder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { super.onStartCommand(intent, flags, startId); Log.d(TAG, "OnStartCommand called"); final int keycode = intent.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1); final boolean castDisconnect = intent.getBooleanExtra(EXTRA_CAST_DISCONNECT, false); final Playable playable = intent.getParcelableExtra(EXTRA_PLAYABLE); if (keycode == -1 && playable == null && !castDisconnect) { Log.e(TAG, "PlaybackService was started with no arguments"); stopSelf(); return Service.START_REDELIVER_INTENT; } if ((flags & Service.START_FLAG_REDELIVERY) != 0) { Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now."); stopForeground(true); } else { if (keycode != -1) { Log.d(TAG, "Received media button event"); handleKeycode(keycode, intent.getIntExtra(MediaButtonReceiver.EXTRA_SOURCE, InputDevice.SOURCE_CLASS_NONE)); } else if (castDisconnect) { castManager.disconnect(); } else { started = true; boolean stream = intent.getBooleanExtra(EXTRA_SHOULD_STREAM, true); boolean startWhenPrepared = intent.getBooleanExtra(EXTRA_START_WHEN_PREPARED, false); boolean prepareImmediately = intent.getBooleanExtra(EXTRA_PREPARE_IMMEDIATELY, false); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); //If the user asks to play External Media, the casting session, if on, should end. if (playable instanceof ExternalMedia) { castManager.disconnect(); } mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, prepareImmediately); } } return Service.START_REDELIVER_INTENT; } /** * Handles media button events */ private void handleKeycode(int keycode, int source) { Log.d(TAG, "Handling keycode: " + keycode); final PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); final PlayerStatus status = info.playerStatus; switch (keycode) { case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: if (status == PlayerStatus.PLAYING) { mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); } else if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { mediaPlayer.resume(); } else if (status == PlayerStatus.PREPARING) { mediaPlayer.setStartWhenPrepared(!mediaPlayer.isStartWhenPrepared()); } else if (status == PlayerStatus.INITIALIZED) { mediaPlayer.setStartWhenPrepared(true); mediaPlayer.prepare(); } break; case KeyEvent.KEYCODE_MEDIA_PLAY: if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { mediaPlayer.resume(); } else if (status == PlayerStatus.INITIALIZED) { mediaPlayer.setStartWhenPrepared(true); mediaPlayer.prepare(); } break; case KeyEvent.KEYCODE_MEDIA_PAUSE: if (status == PlayerStatus.PLAYING) { mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); } break; case KeyEvent.KEYCODE_MEDIA_NEXT: if(source == InputDevice.SOURCE_CLASS_NONE || UserPreferences.shouldHardwareButtonSkip()) { // assume the skip command comes from a notification or the lockscreen // a >| skip button should actually skip mediaPlayer.endPlayback(true, false); } else { // assume skip command comes from a (bluetooth) media button // user actually wants to fast-forward seekDelta(UserPreferences.getFastFowardSecs() * 1000); } break; case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: mediaPlayer.seekDelta(UserPreferences.getFastFowardSecs() * 1000); break; case KeyEvent.KEYCODE_MEDIA_PREVIOUS: case KeyEvent.KEYCODE_MEDIA_REWIND: mediaPlayer.seekDelta(-UserPreferences.getRewindSecs() * 1000); break; case KeyEvent.KEYCODE_MEDIA_STOP: if (status == PlayerStatus.PLAYING) { mediaPlayer.pause(true, true); started = false; } stopForeground(true); // gets rid of persistent notification break; default: Log.d(TAG, "Unhandled key code: " + keycode); if (info.playable != null && info.playerStatus == PlayerStatus.PLAYING) { // only notify the user about an unknown key event if it is actually doing something String message = String.format(getResources().getString(R.string.unknown_media_key), keycode); Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } break; } } /** * Called by a mediaplayer Activity as soon as it has prepared its * mediaplayer. */ public void setVideoSurface(SurfaceHolder sh) { Log.d(TAG, "Setting display"); mediaPlayer.setVideoSurface(sh); } /** * Called when the surface holder of the mediaplayer has to be changed. */ private void resetVideoSurface() { taskManager.cancelPositionSaver(); mediaPlayer.resetVideoSurface(); } public void notifyVideoSurfaceAbandoned() { stopForeground(!UserPreferences.isPersistNotify()); mediaPlayer.resetVideoSurface(); } private final PlaybackServiceTaskManager.PSTMCallback taskManagerCallback = new PlaybackServiceTaskManager.PSTMCallback() { @Override public void positionSaverTick() { saveCurrentPosition(true, PlaybackServiceTaskManager.POSITION_SAVER_WAITING_INTERVAL); } @Override public void onSleepTimerAlmostExpired() { float leftVolume = 0.1f * UserPreferences.getLeftVolume(); float rightVolume = 0.1f * UserPreferences.getRightVolume(); mediaPlayer.setVolume(leftVolume, rightVolume); } @Override public void onSleepTimerExpired() { mediaPlayer.pause(true, true); float leftVolume = UserPreferences.getLeftVolume(); float rightVolume = UserPreferences.getRightVolume(); mediaPlayer.setVolume(leftVolume, rightVolume); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } @Override public void onSleepTimerReset() { float leftVolume = UserPreferences.getLeftVolume(); float rightVolume = UserPreferences.getRightVolume(); mediaPlayer.setVolume(leftVolume, rightVolume); } @Override public void onWidgetUpdaterTick() { updateWidget(); } @Override public void onChapterLoaded(Playable media) { sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); } }; private final PlaybackServiceMediaPlayer.PSMPCallback mediaPlayerCallback = new PlaybackServiceMediaPlayer.PSMPCallback() { @Override public void statusChanged(PlaybackServiceMediaPlayer.PSMPInfo newInfo) { currentMediaType = mediaPlayer.getCurrentMediaType(); updateMediaSession(newInfo.playerStatus); switch (newInfo.playerStatus) { case INITIALIZED: writePlaybackPreferences(); break; case PREPARED: taskManager.startChapterLoader(newInfo.playable); break; case PAUSED: taskManager.cancelPositionSaver(); saveCurrentPosition(false, 0); taskManager.cancelWidgetUpdater(); if ((UserPreferences.isPersistNotify() || isCasting) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { // do not remove notification on pause based on user pref and whether android version supports expanded notifications // Change [Play] button to [Pause] setupNotification(newInfo); } else if (!UserPreferences.isPersistNotify() && !isCasting) { // remove notification on pause stopForeground(true); } writePlayerStatusPlaybackPreferences(); final Playable playable = newInfo.playable; // Gpodder: send play action if(GpodnetPreferences.loggedIn() && playable instanceof FeedMedia) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) .currentDeviceId() .currentTimestamp() .started(startPosition / 1000) .position(getCurrentPosition() / 1000) .total(getDuration() / 1000) .build(); GpodnetPreferences.enqueueEpisodeAction(action); } break; case STOPPED: //setCurrentlyPlayingMedia(PlaybackPreferences.NO_MEDIA_PLAYING); //stopSelf(); break; case PLAYING: Log.d(TAG, "Audiofocus successfully requested"); Log.d(TAG, "Resuming/Starting playback"); taskManager.startPositionSaver(); taskManager.startWidgetUpdater(); writePlayerStatusPlaybackPreferences(); setupNotification(newInfo); started = true; startPosition = mediaPlayer.getPosition(); break; case ERROR: writePlaybackPreferencesNoMediaPlaying(); break; } Intent statusUpdate = new Intent(ACTION_PLAYER_STATUS_CHANGED); // statusUpdate.putExtra(EXTRA_NEW_PLAYER_STATUS, newInfo.playerStatus.ordinal()); sendBroadcast(statusUpdate); updateWidget(); bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED); bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED); } @Override public void shouldStop() { stopSelf(); } @Override public void playbackSpeedChanged(float s) { sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_SPEED_CHANGE, 0); } public void setSpeedAbilityChanged() { sendNotificationBroadcast(NOTIFICATION_TYPE_SET_SPEED_ABILITY_CHANGED, 0); } @Override public void onBufferingUpdate(int percent) { sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_UPDATE, percent); } @Override public void onMediaChanged(boolean reloadUI) { Log.d(TAG, "reloadUI callback reached"); if (reloadUI) { sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0); } PlaybackService.this.updateMediaSessionMetadata(getPlayable()); } @Override public boolean onMediaPlayerInfo(int code, @StringRes int resourceId) { switch (code) { case MediaPlayer.MEDIA_INFO_BUFFERING_START: sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_START, 0); return true; case MediaPlayer.MEDIA_INFO_BUFFERING_END: sendNotificationBroadcast(NOTIFICATION_TYPE_BUFFER_END, 0); return true; case RemotePSMP.CAST_ERROR: sendNotificationBroadcast(NOTIFICATION_TYPE_SHOW_TOAST, resourceId); return true; case RemotePSMP.CAST_ERROR_PRIORITY_HIGH: Toast.makeText(PlaybackService.this, resourceId, Toast.LENGTH_SHORT).show(); return true; default: return false; } } @Override public boolean onMediaPlayerError(Object inObj, int what, int extra) { final String TAG = "PlaybackSvc.onErrorLtsn"; Log.w(TAG, "An error has occured: " + what + " " + extra); if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { mediaPlayer.pause(true, false); } sendNotificationBroadcast(NOTIFICATION_TYPE_ERROR, what); writePlaybackPreferencesNoMediaPlaying(); stopSelf(); return true; } @Override public boolean endPlayback(Playable media, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { PlaybackService.this.endPlayback(media, playNextEpisode, wasSkipped, switchingPlayers); return true; } }; private void endPlayback(final Playable playable, boolean playNextEpisode, boolean wasSkipped, boolean switchingPlayers) { Log.d(TAG, "Playback ended" + (switchingPlayers ? " from switching players": "")); if (playable == null) { Log.e(TAG, "Cannot end playback: media was null"); return; } taskManager.cancelPositionSaver(); boolean isInQueue = false; FeedItem nextItem = null; if (playable instanceof FeedMedia && ((FeedMedia) playable).getItem() != null) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); if (!switchingPlayers) { try { final List<FeedItem> queue = taskManager.getQueue(); isInQueue = QueueAccess.ItemListAccess(queue).contains(item.getId()); nextItem = DBTasks.getQueueSuccessorOfItem(item.getId(), queue); } catch (InterruptedException e) { e.printStackTrace(); // isInQueue remains false } boolean shouldKeep = wasSkipped && UserPreferences.shouldSkipKeepEpisode(); if (!shouldKeep) { // only mark the item as played if we're not keeping it anyways DBWriter.markItemPlayed(item, FeedItem.PLAYED, true); if (isInQueue) { DBWriter.removeQueueItem(PlaybackService.this, item, true); } // Delete episode if enabled if (item.getFeed().getPreferences().getCurrentAutoDelete()) { DBWriter.deleteFeedMediaOfItem(PlaybackService.this, media.getId()); Log.d(TAG, "Episode Deleted"); } } } DBWriter.addItemToPlaybackHistory(media); // auto-flattr if enabled if (isAutoFlattrable(media) && UserPreferences.getAutoFlattrPlayedDurationThreshold() == 1.0f) { DBTasks.flattrItemIfLoggedIn(PlaybackService.this, item); } // gpodder play action if(GpodnetPreferences.loggedIn()) { GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) .currentDeviceId() .currentTimestamp() .started(startPosition / 1000) .position(getDuration() / 1000) .total(getDuration() / 1000) .build(); GpodnetPreferences.enqueueEpisodeAction(action); } } if (!switchingPlayers) { // Load next episode if previous episode was in the queue and if there // is an episode in the queue left. // Start playback immediately if continuous playback is enabled Playable nextMedia = null; boolean loadNextItem = ClientConfig.playbackServiceCallbacks.useQueue() && isInQueue && nextItem != null; playNextEpisode = playNextEpisode && loadNextItem && UserPreferences.isFollowQueue(); if (loadNextItem) { Log.d(TAG, "Loading next item in queue"); nextMedia = nextItem.getMedia(); } final boolean prepareImmediately; final boolean startWhenPrepared; final boolean stream; if (playNextEpisode) { Log.d(TAG, "Playback of next episode will start immediately."); prepareImmediately = startWhenPrepared = true; } else { Log.d(TAG, "No more episodes available to play"); prepareImmediately = startWhenPrepared = false; stopForeground(true); stopWidgetUpdater(); } writePlaybackPreferencesNoMediaPlaying(); if (nextMedia != null) { stream = !nextMedia.localFileAvailable(); mediaPlayer.playMediaObject(nextMedia, stream, startWhenPrepared, prepareImmediately); sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, isCasting ? EXTRA_CODE_CAST : (nextMedia.getMediaType() == MediaType.VIDEO) ? EXTRA_CODE_VIDEO : EXTRA_CODE_AUDIO); } else { sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); mediaPlayer.stop(); //stopSelf(); } } } public void setSleepTimer(long waitingTime, boolean shakeToReset, boolean vibrate) { Log.d(TAG, "Setting sleep timer to " + Long.toString(waitingTime) + " milliseconds"); taskManager.setSleepTimer(waitingTime, shakeToReset, vibrate); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } public void disableSleepTimer() { taskManager.disableSleepTimer(); sendNotificationBroadcast(NOTIFICATION_TYPE_SLEEPTIMER_UPDATE, 0); } private void writePlaybackPreferencesNoMediaPlaying() { SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, PlaybackPreferences.NO_MEDIA_PLAYING); editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, PlaybackPreferences.NO_MEDIA_PLAYING); editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, PlaybackPreferences.NO_MEDIA_PLAYING); editor.putInt( PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, PlaybackPreferences.PLAYER_STATUS_OTHER); editor.commit(); } private int getCurrentPlayerStatusAsInt(PlayerStatus playerStatus) { int playerStatusAsInt; switch (playerStatus) { case PLAYING: playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PLAYING; break; case PAUSED: playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_PAUSED; break; default: playerStatusAsInt = PlaybackPreferences.PLAYER_STATUS_OTHER; } return playerStatusAsInt; } private void writePlaybackPreferences() { Log.d(TAG, "Writing playback preferences"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); PlaybackServiceMediaPlayer.PSMPInfo info = mediaPlayer.getPSMPInfo(); MediaType mediaType = mediaPlayer.getCurrentMediaType(); boolean stream = mediaPlayer.isStreaming(); int playerStatus = getCurrentPlayerStatusAsInt(info.playerStatus); if (info.playable != null) { editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, info.playable.getPlayableType()); editor.putBoolean( PlaybackPreferences.PREF_CURRENT_EPISODE_IS_STREAM, stream); editor.putBoolean( PlaybackPreferences.PREF_CURRENT_EPISODE_IS_VIDEO, mediaType == MediaType.VIDEO); if (info.playable instanceof FeedMedia) { FeedMedia fMedia = (FeedMedia) info.playable; editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, fMedia.getItem().getFeed().getId()); editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, fMedia.getId()); } else { editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, PlaybackPreferences.NO_MEDIA_PLAYING); editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, PlaybackPreferences.NO_MEDIA_PLAYING); } info.playable.writeToPreferences(editor); } else { editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_MEDIA, PlaybackPreferences.NO_MEDIA_PLAYING); editor.putLong(PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEED_ID, PlaybackPreferences.NO_MEDIA_PLAYING); editor.putLong( PlaybackPreferences.PREF_CURRENTLY_PLAYING_FEEDMEDIA_ID, PlaybackPreferences.NO_MEDIA_PLAYING); } editor.putInt( PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); editor.commit(); } private void writePlayerStatusPlaybackPreferences() { Log.d(TAG, "Writing player status playback preferences"); SharedPreferences.Editor editor = PreferenceManager .getDefaultSharedPreferences(getApplicationContext()).edit(); int playerStatus = getCurrentPlayerStatusAsInt(mediaPlayer.getPlayerStatus()); editor.putInt( PlaybackPreferences.PREF_CURRENT_PLAYER_STATUS, playerStatus); editor.commit(); } /** * Send ACTION_PLAYER_STATUS_CHANGED without changing the status attribute. */ private void postStatusUpdateIntent() { sendBroadcast(new Intent(ACTION_PLAYER_STATUS_CHANGED)); } private void sendNotificationBroadcast(int type, int code) { Intent intent = new Intent(ACTION_PLAYER_NOTIFICATION); intent.putExtra(EXTRA_NOTIFICATION_TYPE, type); intent.putExtra(EXTRA_NOTIFICATION_CODE, code); sendBroadcast(intent); } /** * Updates the Media Session for the corresponding status. * @param playerStatus the current {@link PlayerStatus} */ private void updateMediaSession(final PlayerStatus playerStatus) { PlaybackStateCompat.Builder sessionState = new PlaybackStateCompat.Builder(); int state; if (playerStatus != null) { switch (playerStatus) { case PLAYING: state = PlaybackStateCompat.STATE_PLAYING; break; case PREPARED: case PAUSED: state = PlaybackStateCompat.STATE_PAUSED; break; case STOPPED: state = PlaybackStateCompat.STATE_STOPPED; break; case SEEKING: state = PlaybackStateCompat.STATE_FAST_FORWARDING; break; case PREPARING: case INITIALIZING: state = PlaybackStateCompat.STATE_CONNECTING; break; case INITIALIZED: case INDETERMINATE: state = PlaybackStateCompat.STATE_NONE; break; case ERROR: state = PlaybackStateCompat.STATE_ERROR; break; default: state = PlaybackStateCompat.STATE_NONE; break; } } else { state = PlaybackStateCompat.STATE_NONE; } sessionState.setState(state, mediaPlayer.getPosition(), mediaPlayer.getPlaybackSpeed()); sessionState.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_REWIND | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_SKIP_TO_NEXT); mediaSession.setPlaybackState(sessionState.build()); } /** * Used by updateMediaSessionMetadata to load notification data in another thread. */ private Thread mediaSessionSetupThread; private void updateMediaSessionMetadata(final Playable p) { if (p == null || mediaSession == null) { return; } if (mediaSessionSetupThread != null) { mediaSessionSetupThread.interrupt(); } Runnable mediaSessionSetupTask = () -> { MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder(); builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle()); builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle()); builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration()); builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle()); builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle()); if (p.getImageUri() != null && UserPreferences.setLockscreenBackground()) { builder.putString(MediaMetadataCompat.METADATA_KEY_ART_URI, p.getImageUri().toString()); try { if (isCasting) { Bitmap art = Glide.with(this) .load(p.getImageUri()) .asBitmap() .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) .into(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) .get(); builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); } else { WindowManager wm = (WindowManager) getSystemService(Context.WINDOW_SERVICE); Display display = wm.getDefaultDisplay(); Bitmap art = Glide.with(this) .load(p.getImageUri()) .asBitmap() .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) .centerCrop() .into(display.getWidth(), display.getHeight()) .get(); builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, art); } } catch (Throwable tr) { Log.e(TAG, Log.getStackTraceString(tr)); } } if (!Thread.currentThread().isInterrupted() && started) { mediaSession.setMetadata(builder.build()); } }; mediaSessionSetupThread = new Thread(mediaSessionSetupTask); mediaSessionSetupThread.start(); } /** * Used by setupNotification to load notification data in another thread. */ private Thread notificationSetupThread; /** * Prepares notification and starts the service in the foreground. */ private void setupNotification(final PlaybackServiceMediaPlayer.PSMPInfo info) { final PendingIntent pIntent = PendingIntent.getActivity(this, 0, PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT); if (notificationSetupThread != null) { notificationSetupThread.interrupt(); } Runnable notificationSetupTask = new Runnable() { Bitmap icon = null; @Override public void run() { Log.d(TAG, "Starting background work"); if (android.os.Build.VERSION.SDK_INT >= 11) { if (info.playable != null) { int iconSize = getResources().getDimensionPixelSize( android.R.dimen.notification_large_icon_width); try { icon = Glide.with(PlaybackService.this) .load(info.playable.getImageUri()) .asBitmap() .diskCacheStrategy(ApGlideSettings.AP_DISK_CACHE_STRATEGY) .centerCrop() .into(iconSize, iconSize) .get(); } catch (Throwable tr) { Log.e(TAG, "Error loading the media icon for the notification", tr); } } } if (icon == null) { icon = BitmapFactory.decodeResource(getApplicationContext().getResources(), ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext())); } if (mediaPlayer == null) { return; } PlayerStatus playerStatus = mediaPlayer.getPlayerStatus(); final int smallIcon = ClientConfig.playbackServiceCallbacks.getNotificationIconResource(getApplicationContext()); if (!Thread.currentThread().isInterrupted() && started && info.playable != null) { String contentText = info.playable.getEpisodeTitle(); String contentTitle = info.playable.getFeedTitle(); Notification notification; // Builder is v7, even if some not overwritten methods return its parent's v4 interface NotificationCompat.Builder notificationBuilder = (NotificationCompat.Builder) new NotificationCompat.Builder( PlaybackService.this) .setContentTitle(contentTitle) .setContentText(contentText) .setOngoing(false) .setContentIntent(pIntent) .setLargeIcon(icon) .setSmallIcon(smallIcon) .setWhen(0) // we don't need the time .setPriority(UserPreferences.getNotifyPriority()); // set notification priority IntList compactActionList = new IntList(); int numActions = 0; // we start and 0 and then increment by 1 for each call to addAction if (isCasting) { Intent stopCastingIntent = new Intent(PlaybackService.this, PlaybackService.class); stopCastingIntent.putExtra(EXTRA_CAST_DISCONNECT, true); PendingIntent stopCastingPendingIntent = PendingIntent.getService(PlaybackService.this, numActions, stopCastingIntent, PendingIntent.FLAG_UPDATE_CURRENT); notificationBuilder.addAction(R.drawable.ic_media_cast_disconnect, getString(R.string.cast_disconnect_label), stopCastingPendingIntent); numActions++; } // always let them rewind PendingIntent rewindButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_REWIND, numActions); notificationBuilder.addAction(android.R.drawable.ic_media_rew, getString(R.string.rewind_label), rewindButtonPendingIntent); if(UserPreferences.showRewindOnCompactNotification()) { compactActionList.add(numActions); } numActions++; if (playerStatus == PlayerStatus.PLAYING) { PendingIntent pauseButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_PAUSE, numActions); notificationBuilder.addAction(android.R.drawable.ic_media_pause, //pause action getString(R.string.pause_label), pauseButtonPendingIntent); compactActionList.add(numActions++); } else { PendingIntent playButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_PLAY, numActions); notificationBuilder.addAction(android.R.drawable.ic_media_play, //play action getString(R.string.play_label), playButtonPendingIntent); compactActionList.add(numActions++); } // ff follows play, then we have skip (if it's present) PendingIntent ffButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions); notificationBuilder.addAction(android.R.drawable.ic_media_ff, getString(R.string.fast_forward_label), ffButtonPendingIntent); if(UserPreferences.showFastForwardOnCompactNotification()) { compactActionList.add(numActions); } numActions++; if (UserPreferences.isFollowQueue()) { PendingIntent skipButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_NEXT, numActions); notificationBuilder.addAction(android.R.drawable.ic_media_next, getString(R.string.skip_episode_label), skipButtonPendingIntent); if(UserPreferences.showSkipOnCompactNotification()) { compactActionList.add(numActions); } numActions++; } PendingIntent stopButtonPendingIntent = getPendingIntentForMediaAction( KeyEvent.KEYCODE_MEDIA_STOP, numActions); notificationBuilder.setStyle(new android.support.v7.app.NotificationCompat.MediaStyle() .setMediaSession(mediaSession.getSessionToken()) .setShowActionsInCompactView(compactActionList.toArray()) .setShowCancelButton(true) .setCancelButtonIntent(stopButtonPendingIntent)) .setVisibility(Notification.VISIBILITY_PUBLIC) .setColor(Notification.COLOR_DEFAULT); notification = notificationBuilder.build(); if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARING || playerStatus == PlayerStatus.SEEKING || isCasting) { startForeground(NOTIFICATION_ID, notification); } else { stopForeground(false); NotificationManager mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); mNotificationManager.notify(NOTIFICATION_ID, notification); } Log.d(TAG, "Notification set up"); } } }; notificationSetupThread = new Thread(notificationSetupTask); notificationSetupThread.start(); } private PendingIntent getPendingIntentForMediaAction(int keycodeValue, int requestCode) { Intent intent = new Intent( PlaybackService.this, PlaybackService.class); intent.putExtra( MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue); return PendingIntent .getService(PlaybackService.this, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT); } /** * Persists the current position and last played time of the media file. * * @param updatePlayedDuration true if played_duration should be updated. This applies only to FeedMedia objects * @param deltaPlayedDuration value by which played_duration should be increased. */ private synchronized void saveCurrentPosition(boolean updatePlayedDuration, int deltaPlayedDuration) { int position = getCurrentPosition(); int duration = getDuration(); float playbackSpeed = getCurrentPlaybackSpeed(); final Playable playable = mediaPlayer.getPlayable(); if (position != INVALID_TIME && duration != INVALID_TIME && playable != null) { Log.d(TAG, "Saving current position to " + position); if (updatePlayedDuration && playable instanceof FeedMedia) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); media.setPlayedDuration(media.getPlayedDuration() + ((int) (deltaPlayedDuration * playbackSpeed))); // Auto flattr if (isAutoFlattrable(media) && (media.getPlayedDuration() > UserPreferences.getAutoFlattrPlayedDurationThreshold() * duration)) { Log.d(TAG, "saveCurrentPosition: performing auto flattr since played duration " + Integer.toString(media.getPlayedDuration()) + " is " + UserPreferences.getAutoFlattrPlayedDurationThreshold() * 100 + "% of file duration " + Integer.toString(duration)); DBTasks.flattrItemIfLoggedIn(this, item); } } playable.saveCurrentPosition( PreferenceManager.getDefaultSharedPreferences(getApplicationContext()), position, System.currentTimeMillis()); } } private void stopWidgetUpdater() { taskManager.cancelWidgetUpdater(); sendBroadcast(new Intent(STOP_WIDGET_UPDATE)); } private void updateWidget() { PlaybackService.this.sendBroadcast(new Intent( FORCE_WIDGET_UPDATE)); } public boolean sleepTimerActive() { return taskManager.isSleepTimerActive(); } public long getSleepTimerTimeLeft() { return taskManager.getSleepTimerTimeLeft(); } private void bluetoothNotifyChange(PlaybackServiceMediaPlayer.PSMPInfo info, String whatChanged) { boolean isPlaying = false; if (info.playerStatus == PlayerStatus.PLAYING) { isPlaying = true; } if (info.playable != null) { Intent i = new Intent(whatChanged); i.putExtra("id", 1); i.putExtra("artist", ""); i.putExtra("album", info.playable.getFeedTitle()); i.putExtra("track", info.playable.getEpisodeTitle()); i.putExtra("playing", isPlaying); final List<FeedItem> queue = taskManager.getQueueIfLoaded(); if (queue != null) { i.putExtra("ListSize", queue.size()); } i.putExtra("duration", info.playable.getDuration()); i.putExtra("position", info.playable.getPosition()); sendBroadcast(i); } } /** * Pauses playback when the headset is disconnected and the preference is * set */ private final BroadcastReceiver headsetDisconnected = new BroadcastReceiver() { private static final String TAG = "headsetDisconnected"; private static final int UNPLUGGED = 0; private static final int PLUGGED = 1; @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), Intent.ACTION_HEADSET_PLUG)) { int state = intent.getIntExtra("state", -1); if (state != -1) { Log.d(TAG, "Headset plug event. State is " + state); if (state == UNPLUGGED) { Log.d(TAG, "Headset was unplugged during playback."); pauseIfPauseOnDisconnect(); } else if (state == PLUGGED) { Log.d(TAG, "Headset was plugged in during playback."); unpauseIfPauseOnDisconnect(false); } } else { Log.e(TAG, "Received invalid ACTION_HEADSET_PLUG intent"); } } } }; private final BroadcastReceiver bluetoothStateUpdated = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (TextUtils.equals(intent.getAction(), BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED)) { int state = intent.getIntExtra(BluetoothA2dp.EXTRA_STATE, -1); if (state == BluetoothA2dp.STATE_CONNECTED) { Log.d(TAG, "Received bluetooth connection intent"); unpauseIfPauseOnDisconnect(true); } } } } }; private final BroadcastReceiver audioBecomingNoisy = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // sound is about to change, eg. bluetooth -> speaker Log.d(TAG, "Pausing playback because audio is becoming noisy"); pauseIfPauseOnDisconnect(); } // android.media.AUDIO_BECOMING_NOISY }; /** * Pauses playback if PREF_PAUSE_ON_HEADSET_DISCONNECT was set to true. */ private void pauseIfPauseOnDisconnect() { if (UserPreferences.isPauseOnHeadsetDisconnect()) { if (mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING) { transientPause = true; } mediaPlayer.pause(!UserPreferences.isPersistNotify(), true); } } /** * @param bluetooth true if the event for unpausing came from bluetooth */ private void unpauseIfPauseOnDisconnect(boolean bluetooth) { if (transientPause) { transientPause = false; if (!bluetooth && UserPreferences.isUnpauseOnHeadsetReconnect()) { mediaPlayer.resume(); } else if (bluetooth && UserPreferences.isUnpauseOnBluetoothReconnect()){ // let the user know we've started playback again... Vibrator v = (Vibrator) getApplicationContext().getSystemService(Context.VIBRATOR_SERVICE); if(v != null) { v.vibrate(500); } mediaPlayer.resume(); } } } private final BroadcastReceiver shutdownReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SHUTDOWN_PLAYBACK_SERVICE)) { stopSelf(); } } }; private final BroadcastReceiver skipCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_SKIP_CURRENT_EPISODE)) { Log.d(TAG, "Received SKIP_CURRENT_EPISODE intent"); mediaPlayer.endPlayback(true, false); } } }; private final BroadcastReceiver pauseResumeCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_RESUME_PLAY_CURRENT_EPISODE)) { Log.d(TAG, "Received RESUME_PLAY_CURRENT_EPISODE intent"); mediaPlayer.resume(); } } }; private final BroadcastReceiver pausePlayCurrentEpisodeReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (TextUtils.equals(intent.getAction(), ACTION_PAUSE_PLAY_CURRENT_EPISODE)) { Log.d(TAG, "Received PAUSE_PLAY_CURRENT_EPISODE intent"); mediaPlayer.pause(false, false); } } }; public static MediaType getCurrentMediaType() { return currentMediaType; } public static boolean isCasting() { return isCasting; } public void resume() { mediaPlayer.resume(); } public void prepare() { mediaPlayer.prepare(); } public void pause(boolean abandonAudioFocus, boolean reinit) { mediaPlayer.pause(abandonAudioFocus, reinit); } public void reinit() { mediaPlayer.reinit(); } public PlaybackServiceMediaPlayer.PSMPInfo getPSMPInfo() { return mediaPlayer.getPSMPInfo(); } public PlayerStatus getStatus() { return mediaPlayer.getPlayerStatus(); } public Playable getPlayable() { return mediaPlayer.getPlayable(); } public boolean canSetSpeed() { return mediaPlayer.canSetSpeed(); } public void setSpeed(float speed) { mediaPlayer.setSpeed(speed); } public void setVolume(float leftVolume, float rightVolume) { mediaPlayer.setVolume(leftVolume, rightVolume); } public float getCurrentPlaybackSpeed() { return mediaPlayer.getPlaybackSpeed(); } public boolean canDownmix() { return mediaPlayer.canDownmix(); } public void setDownmix(boolean enable) { mediaPlayer.setDownmix(enable); } public boolean isStartWhenPrepared() { return mediaPlayer.isStartWhenPrepared(); } public void setStartWhenPrepared(boolean s) { mediaPlayer.setStartWhenPrepared(s); } public void seekTo(final int t) { if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING && GpodnetPreferences.loggedIn()) { final Playable playable = mediaPlayer.getPlayable(); if (playable instanceof FeedMedia) { FeedMedia media = (FeedMedia) playable; FeedItem item = media.getItem(); GpodnetEpisodeAction action = new GpodnetEpisodeAction.Builder(item, Action.PLAY) .currentDeviceId() .currentTimestamp() .started(startPosition / 1000) .position(getCurrentPosition() / 1000) .total(getDuration() / 1000) .build(); GpodnetPreferences.enqueueEpisodeAction(action); } } mediaPlayer.seekTo(t); if(mediaPlayer.getPlayerStatus() == PlayerStatus.PLAYING ) { startPosition = t; } } public void seekDelta(final int d) { mediaPlayer.seekDelta(d); } /** * @see LocalPSMP#seekToChapter(de.danoeh.antennapod.core.feed.Chapter) */ public void seekToChapter(Chapter c) { mediaPlayer.seekToChapter(c); } /** * call getDuration() on mediaplayer or return INVALID_TIME if player is in * an invalid state. */ public int getDuration() { return mediaPlayer.getDuration(); } /** * call getCurrentPosition() on mediaplayer or return INVALID_TIME if player * is in an invalid state. */ public int getCurrentPosition() { return mediaPlayer.getPosition(); } public boolean isStreaming() { return mediaPlayer.isStreaming(); } public Pair<Integer, Integer> getVideoSize() { return mediaPlayer.getVideoSize(); } private boolean isAutoFlattrable(FeedMedia media) { if (media != null) { FeedItem item = media.getItem(); return item != null && FlattrUtils.hasToken() && UserPreferences.isAutoFlattr() && item.getPaymentLink() != null && item.getFlattrStatus().getUnflattred(); } else { return false; } } private CastConsumer castConsumer = new DefaultCastConsumer() { @Override public void onApplicationConnected(ApplicationMetadata appMetadata, String sessionId, boolean wasLaunched) { PlaybackService.this.onCastAppConnected(wasLaunched); } @Override public void onDisconnectionReason(int reason) { Log.d(TAG, "onDisconnectionReason() with code " + reason); // This is our final chance to update the underlying stream position // In onDisconnected(), the underlying CastPlayback#mVideoCastConsumer // is disconnected and hence we update our local value of stream position // to the latest position. if (mediaPlayer != null) { saveCurrentPosition(false, 0); infoBeforeCastDisconnection = mediaPlayer.getPSMPInfo(); if (reason != BaseCastManager.DISCONNECT_REASON_EXPLICIT && infoBeforeCastDisconnection.playerStatus == PlayerStatus.PLAYING) { // If it's NOT based on user action, we shouldn't automatically resume local playback infoBeforeCastDisconnection.playerStatus = PlayerStatus.PAUSED; } } } @Override public void onDisconnected() { Log.d(TAG, "onDisconnected()"); isCasting = false; PlaybackServiceMediaPlayer.PSMPInfo info = infoBeforeCastDisconnection; infoBeforeCastDisconnection = null; if (info == null && mediaPlayer != null) { info = mediaPlayer.getPSMPInfo(); } if (info == null) { info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null); } switchMediaPlayer(new LocalPSMP(PlaybackService.this, mediaPlayerCallback), info, true); if (info.playable != null) { sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, info.playable.getMediaType() == MediaType.AUDIO ? EXTRA_CODE_AUDIO : EXTRA_CODE_VIDEO); } else { Log.d(TAG, "Cast session disconnected, but no current media"); sendNotificationBroadcast(NOTIFICATION_TYPE_PLAYBACK_END, 0); } // hardware volume buttons control the local device volume mediaRouter.setMediaSessionCompat(null); unregisterWifiBroadcastReceiver(); PlayerStatus status = info.playerStatus; if ((status == PlayerStatus.PLAYING || status == PlayerStatus.SEEKING || status == PlayerStatus.PREPARING || UserPreferences.isPersistNotify()) && android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { setupNotification(info); } else if (!UserPreferences.isPersistNotify()){ stopForeground(true); } } }; private final MediaSessionCompat.Callback sessionCallback = new MediaSessionCompat.Callback() { private static final String TAG = "MediaSessionCompat"; @Override public void onPlay() { Log.d(TAG, "onPlay()"); PlayerStatus status = getStatus(); if (status == PlayerStatus.PAUSED || status == PlayerStatus.PREPARED) { resume(); } else if (status == PlayerStatus.INITIALIZED) { setStartWhenPrepared(true); prepare(); } } @Override public void onPause() { Log.d(TAG, "onPause()"); if (getStatus() == PlayerStatus.PLAYING) { pause(false, true); } if (UserPreferences.isPersistNotify()) { pause(false, true); } else { pause(true, true); } } @Override public void onStop() { Log.d(TAG, "onStop()"); mediaPlayer.stop(); } @Override public void onSkipToPrevious() { Log.d(TAG, "onSkipToPrevious()"); seekDelta(-UserPreferences.getRewindSecs() * 1000); } @Override public void onRewind() { Log.d(TAG, "onRewind()"); seekDelta(-UserPreferences.getRewindSecs() * 1000); } @Override public void onFastForward() { Log.d(TAG, "onFastForward()"); seekDelta(UserPreferences.getFastFowardSecs() * 1000); } @Override public void onSkipToNext() { Log.d(TAG, "onSkipToNext()"); if(UserPreferences.shouldHardwareButtonSkip()) { mediaPlayer.endPlayback(true, false); } else { seekDelta(UserPreferences.getFastFowardSecs() * 1000); } } @Override public void onSeekTo(long pos) { Log.d(TAG, "onSeekTo()"); seekTo((int) pos); } @Override public boolean onMediaButtonEvent(final Intent mediaButton) { Log.d(TAG, "onMediaButtonEvent(" + mediaButton + ")"); if (mediaButton != null) { KeyEvent keyEvent = (KeyEvent) mediaButton.getExtras().get(Intent.EXTRA_KEY_EVENT); if (keyEvent != null && keyEvent.getAction() == KeyEvent.ACTION_DOWN && keyEvent.getRepeatCount() == 0){ handleKeycode(keyEvent.getKeyCode(), keyEvent.getSource()); } } return false; } }; private void onCastAppConnected(boolean wasLaunched) { Log.d(TAG, "A cast device application was " + (wasLaunched ? "launched" : "joined")); isCasting = true; PlaybackServiceMediaPlayer.PSMPInfo info = null; if (mediaPlayer != null) { info = mediaPlayer.getPSMPInfo(); if (info.playerStatus == PlayerStatus.PLAYING) { // could be pause, but this way we make sure the new player will get the correct position, // since pause runs asynchronously and we could be directing the new player to play even before // the old player gives us back the position. saveCurrentPosition(false, 0); } } if (info == null) { info = new PlaybackServiceMediaPlayer.PSMPInfo(PlayerStatus.STOPPED, null); } sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, EXTRA_CODE_CAST); switchMediaPlayer(new RemotePSMP(PlaybackService.this, mediaPlayerCallback), info, wasLaunched); // hardware volume buttons control the remote device volume mediaRouter.setMediaSessionCompat(mediaSession); registerWifiBroadcastReceiver(); setupNotification(info); } private void switchMediaPlayer(@NonNull PlaybackServiceMediaPlayer newPlayer, @NonNull PlaybackServiceMediaPlayer.PSMPInfo info, boolean wasLaunched) { if (mediaPlayer != null) { mediaPlayer.endPlayback(true, true); mediaPlayer.shutdownQuietly(); } mediaPlayer = newPlayer; Log.d(TAG, "switched to " + mediaPlayer.getClass().getSimpleName()); if (!wasLaunched) { PlaybackServiceMediaPlayer.PSMPInfo candidate = mediaPlayer.getPSMPInfo(); if (candidate.playable != null && candidate.playerStatus.isAtLeast(PlayerStatus.PREPARING)) { // do not automatically send new media to cast device info.playable = null; } } if (info.playable != null) { mediaPlayer.playMediaObject(info.playable, !info.playable.localFileAvailable(), info.playerStatus == PlayerStatus.PLAYING, info.playerStatus.isAtLeast(PlayerStatus.PREPARING)); } } private void registerWifiBroadcastReceiver() { if (wifiBroadcastReceiver != null) { return; } wifiBroadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) { NetworkInfo info = intent.getParcelableExtra(WifiManager.EXTRA_NETWORK_INFO); boolean isConnected = info.isConnected(); //apparently this method gets called twice when a change happens, but one run is enough. if (isConnected && !wifiConnectivity) { wifiConnectivity = true; castManager.startCastDiscovery(); castManager.reconnectSessionIfPossible(RECONNECTION_ATTEMPT_PERIOD_S, NetworkUtils.getWifiSsid()); } else { wifiConnectivity = isConnected; } } } }; registerReceiver(wifiBroadcastReceiver, new IntentFilter(WifiManager.NETWORK_STATE_CHANGED_ACTION)); } private void unregisterWifiBroadcastReceiver() { if (wifiBroadcastReceiver != null) { unregisterReceiver(wifiBroadcastReceiver); wifiBroadcastReceiver = null; } } private SharedPreferences.OnSharedPreferenceChangeListener prefListener = (sharedPreferences, key) -> { if (UserPreferences.PREF_CAST_ENABLED.equals(key)) { if (!UserPreferences.isCastEnabled()) { if (castManager.isConnecting() || castManager.isConnected()) { Log.d(TAG, "Disconnecting cast device due to a change in user preferences"); castManager.disconnect(); } } } else if (UserPreferences.PREF_LOCKSCREEN_BACKGROUND.equals(key)) { updateMediaSessionMetadata(getPlayable()); } }; }